#pragma once

#include <cstdint>
#include <chrono>
#include <string>
#include <system_error>


namespace evm { namespace fs {

class file_status;
class path;
class filesystem_error;
class directory_entry;
class directory_iterator;
class recursive_directory_iterator;

using std::string;
using std::system_error;
using std::error_code;

class filesystem_error : public system_error
{
public:
    filesystem_error(const string& msg, error_code ec)
        : system_error(ec, msg) {}

    filesystem_error(const string& msg, const string& p1, error_code ec)
        : system_error(ec, msg), path1_(p1) {}

    filesystem_error(const string& msg, const string& p1, const string& p2, error_code ec)
        : system_error(ec, msg), path1_(p1), path2_(p2) {}

    const string& path1() const noexcept { return path1_; }
    const string& path2() const noexcept { return path2_; }
    const char* what() const noexcept { return what_.c_str(); }

private:
    string gen_what() {
        std::string what = "filesystem error: ";
        what += system_error::what();
        if (!path1_.empty())
            what += " [" + path1_ + ']';
        if (!path2_.empty())
            what += " [" + path2_ + ']';
        return what;
    }

    string path1_;
    string path2_;
    string what_ = gen_what();
};

struct space_info
{
    uintmax_t capacity;
    uintmax_t free;
    uintmax_t available;
};

enum class file_type : signed char {
    none = 0,        // indicates that the file status has not been evaluated yet, or an error occurred when evaluating it
    not_found = -1,  // indicates that the file was not found (this is not considered an error)

    regular = 1,     // a regular file
    directory,       // a directory
    symlink,         // a symbolic link
    block,           // a block special file
    character,       // a character special file
    fifo,            // a FIFO (also known as pipe) file
    socket,          // a socket file

    unknown,         // file does exist, but isn't one of the above types,
                     // or we don't have strong enough permission to find its type
};

enum class copy_options : unsigned short {
    none = 0,
    skip_existing = 1,
    overwrite_existing = 2,
    update_existing = 4,
    recursive = 8,
    copy_symlinks = 16,
    skip_symlinks = 32,
    directories_only = 64,
    create_symlinks = 128,
    create_hard_links = 256
};

constexpr copy_options
operator&(copy_options x, copy_options y) {
    using utype = typename std::underlying_type<copy_options>::type;
    return static_cast<copy_options>(static_cast<utype>(x) & static_cast<utype>(y));
}

constexpr copy_options
operator|(copy_options x, copy_options y) {
    using utype = typename std::underlying_type<copy_options>::type;
    return static_cast<copy_options>(static_cast<utype>(x) | static_cast<utype>(y));
}

constexpr copy_options
operator^(copy_options x, copy_options y) {
    using utype = typename std::underlying_type<copy_options>::type;
    return static_cast<copy_options>(static_cast<utype>(x) ^ static_cast<utype>(y));
}

constexpr copy_options
operator~(copy_options x) {
    using utype = typename std::underlying_type<copy_options>::type;
    return static_cast<copy_options>(~static_cast<utype>(x));
}

inline copy_options&
operator&=(copy_options& x, copy_options y) { return x = x & y; }

inline copy_options&
operator|=(copy_options& x, copy_options y) { return x = x | y; }

inline copy_options&
operator^=(copy_options& x, copy_options y) { return x = x ^ y; }

enum class perms : unsigned {
    none = 0,       // file_not_found is no_perms rather than perms_not_known

    // POSIX equivalent macros given in comments.
    // Values are from POSIX and are given in octal per the POSIX standard.

    // permission bits
    owner_read = 0400,  // S_IRUSR, Read permission, owner
    owner_write = 0200, // S_IWUSR, Write permission, owner
    owner_exec = 0100,   // S_IXUSR, Execute/search permission, owner
    owner_all = 0700,   // S_IRWXU, Read, write, execute/search permissions by owner

    group_read = 040,   // S_IRGRP, Read permission, group
    group_write = 020,  // S_IWGRP, Write permission, group
    group_exec = 010,    // S_IXGRP, Execute/search permission, group
    group_all = 070,    // S_IRWXG, Read, write, execute/search permissions by group

    others_read = 04,   // S_IROTH, Read permission, others
    others_write = 02,  // S_IWOTH, Write permission, others
    others_exec = 01,    // S_IXOTH, Execute/search permission, others
    others_all = 07,    // S_IRWXO, Read, write, execute/search permissions by others

    all = 0777,     // owner_all | group_all | others_all

    // other POSIX bits
            set_uid = 04000, // S_ISUID, Set user ID to file owner user ID on execution
    set_gid = 02000, // S_ISGID, Set group ID to file's user group ID on execution
    sticky_bit     = 01000, // S_ISVT, POSIX XSI specifies that when set on a directory,
    // only file owners may delete files even if the directory
    // is writeable to others (used with /tmp)

    mask = 07777,     // all_all | set_uid | set_gid | sticky_bit

    unknown = 0xFFFF, // Unknown permissions (e.g. when file_status is created without permissions)

    // options for permissions() function
    // choose add_perms or remove_perms, not both; if neither add_perms
    // nor remove_perms is given, replace the current bits with
    // the given bits.
            add_perms = 0x10000,     // adds the given permission bits to the current bits
    remove_perms = 0x20000,  // removes the given permission bits from the current bits;

    resolve_symlink = 0x40000  // on POSIX, don't resolve symlinks; implied on Windows
};

constexpr perms
operator&(perms x, perms y) {
    using utype = typename std::underlying_type<perms>::type;
    return static_cast<perms>(static_cast<utype>(x) & static_cast<utype>(y));
}

constexpr perms
operator|(perms x, perms y) {
    using utype = typename std::underlying_type<perms>::type;
    return static_cast<perms>(static_cast<utype>(x) | static_cast<utype>(y));
}

constexpr perms
operator^(perms x, perms y) {
    using utype = typename std::underlying_type<perms>::type;
    return static_cast<perms>(static_cast<utype>(x) ^ static_cast<utype>(y));
}

constexpr perms
operator~(perms x) {
    using utype = typename std::underlying_type<perms>::type;
    return static_cast<perms>(~static_cast<utype>(x));
}

inline perms&
operator&=(perms& x, perms y) { return x = x & y; }

inline perms&
operator|=(perms& x, perms y) { return x = x | y; }

inline perms&
operator^=(perms& x, perms y) { return x = x ^ y; }

enum class directory_options : unsigned char {
    none = 0, follow_directory_symlink = 1, skip_permission_denied = 2
};

constexpr directory_options
operator&(directory_options x, directory_options y) {
    using utype = typename std::underlying_type<directory_options>::type;
    return static_cast<directory_options>(static_cast<utype>(x) & static_cast<utype>(y));
}

constexpr directory_options
operator|(directory_options x, directory_options y) {
    using utype = typename std::underlying_type<directory_options>::type;
    return static_cast<directory_options>(static_cast<utype>(x) | static_cast<utype>(y));
}

constexpr directory_options
operator^(directory_options x, directory_options y) {
    using utype = typename std::underlying_type<directory_options>::type;
    return static_cast<directory_options>(static_cast<utype>(x) ^ static_cast<utype>(y));
}

constexpr directory_options
operator~(directory_options x) {
    using utype = typename std::underlying_type<directory_options>::type;
    return static_cast<directory_options>(~static_cast<utype>(x));
}

inline directory_options&
operator&=(directory_options& x, directory_options y) { return x = x & y; }

inline directory_options&
operator|=(directory_options& x, directory_options y) { return x = x | y; }

inline directory_options&
operator^=(directory_options& x, directory_options y) { return x = x ^ y; }

typedef std::chrono::time_point<std::chrono::system_clock> file_time_type;

string current_path(bool raise = false);

bool copy(const string&, const string&, copy_options);

bool copy_file(const string&, const string&, copy_options);

bool status_known(file_status) noexcept;

file_status status(const string& p, bool raise = false);
file_status status(const string&, error_code&);

file_status symlink_status(const string& p, bool raise = false);
file_status symlink_status(const string&, error_code&);

bool is_regular_file(file_status) noexcept;

bool is_symlink(file_status) noexcept;

}}
